'use strict';

const Path = require('path');
const Zlib = require('zlib');

const Boom = require('@hapi/boom');
const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
const Code = require('@hapi/code');
const Handlebars = require('handlebars');
const Hapi = require('..');
const Hoek = require('@hapi/hoek');
const Inert = require('@hapi/inert');
const Lab = require('@hapi/lab');
const Vision = require('@hapi/vision');
const Wreck = require('@hapi/wreck');

const Pkg = require('../package.json');


const internals = {};


const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;


describe('Server', () => {

    describe('auth', () => {

        it('adds auth strategy via plugin', async () => {

            const server = Hapi.server();
            server.route({
                method: 'GET',
                path: '/',
                handler: () => 'authenticated!'
            });

            await server.register(internals.plugins.auth);

            const res1 = await server.inject('/');
            expect(res1.statusCode).to.equal(401);

            const res2 = await server.inject({ method: 'GET', url: '/', headers: { authorization: 'Basic ' + (Buffer.from('john:12345', 'utf8')).toString('base64') } });
            expect(res2.statusCode).to.equal(200);
            expect(res2.result).to.equal('authenticated!');
        });
    });

    describe('bind()', () => {

        it('sets plugin context', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    const bind = {
                        value: 'in context',
                        suffix: ' throughout'
                    };

                    srv.bind(bind);

                    srv.route({
                        method: 'GET',
                        path: '/',
                        handler: function () {

                            return this.value;
                        }
                    });

                    const preResponse = function (request, h) {

                        return request.response.source + this.suffix;
                    };

                    srv.ext('onPreResponse', preResponse);
                }
            };

            const server = Hapi.server();
            await server.register(test);

            const res = await server.inject('/');
            expect(res.result).to.equal('in context throughout');
        });
    });

    describe('cache()', () => {

        it('provisions a server cache', async () => {

            const server = Hapi.server();
            const cache = server.cache({ segment: 'test', expiresIn: 1000 });
            await server.initialize();

            await cache.set('a', 'going in', 0);
            const value = await cache.get('a');
            expect(value).to.equal('going in');
        });

        it('throws when missing segment', () => {

            const server = Hapi.server();
            expect(() => {

                server.cache({ expiresIn: 1000 });
            }).to.throw('Missing cache segment name');
        });

        it('provisions a server cache with custom partition', async () => {

            const server = Hapi.server({ cache: { provider: { constructor: CatboxMemory, options: { partition: 'hapi-test-other' } } } });
            const cache = server.cache({ segment: 'test', expiresIn: 1000 });
            await server.initialize();

            await cache.set('a', 'going in', 0);
            const value = await cache.get('a');
            expect(value).to.equal('going in');
            expect(cache._cache.connection.settings.partition).to.equal('hapi-test-other');
        });

        it('throws when allocating an invalid cache segment', () => {

            const server = Hapi.server();
            expect(() => {

                server.cache({ segment: 'a', expiresAt: '12:00', expiresIn: 1000 });
            }).throws();
        });

        it('allows allocating a cache segment with empty options', () => {

            const server = Hapi.server();
            expect(() => {

                server.cache({ segment: 'a' });
            }).to.not.throw();
        });

        it('allows reusing the same cache segment (server)', () => {

            const server = Hapi.server({ cache: { provider: CatboxMemory, shared: true } });
            expect(() => {

                server.cache({ segment: 'a', expiresIn: 1000 });
                server.cache({ segment: 'a', expiresIn: 1000 });
            }).to.not.throw();
        });

        it('allows reusing the same cache segment (cache)', () => {

            const server = Hapi.server();
            expect(() => {

                server.cache({ segment: 'a', expiresIn: 1000 });
                server.cache({ segment: 'a', expiresIn: 1000, shared: true });
            }).to.not.throw();
        });

        it('uses plugin cache interface', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    const cache = srv.cache({ expiresIn: 50 });
                    srv.expose({
                        get: function (key) {

                            return cache.get(key);
                        },
                        set: function (key, value) {

                            return cache.set(key, value, 0);
                        }
                    });
                }
            };

            const server = Hapi.server();
            await server.register(test);
            await server.initialize();

            await server.plugins.test.set('a', '1');
            const value1 = await server.plugins.test.get('a');
            expect(value1).to.equal('1');

            await Hoek.wait(600);
            const value2 = await server.plugins.test.get('a');
            expect(value2).to.equal(null);
        });

        it('emits a cache policy event with default cache provision', async () => {

            const server = Hapi.server();
            const cachePolicyEvent = server.events.once('cachePolicy');

            const cache = server.cache({ segment: 'test', expiresIn: 1000 });

            const [policy, cacheName, segment] = await cachePolicyEvent;
            expect(policy).to.shallow.equal(cache);
            expect(cacheName).to.equal(undefined);
            expect(segment).to.equal('test');
        });

        it('emits a cache policy event with named cache provision', async () => {

            const server = Hapi.server();
            await server.cache.provision({ provider: CatboxMemory, name: 'named' });
            const cachePolicyEvent = server.events.once('cachePolicy');

            const cache = server.cache({ cache: 'named', segment: 'test', expiresIn: 1000 });

            const [policy, cacheName, segment] = await cachePolicyEvent;
            expect(policy).to.shallow.equal(cache);
            expect(cacheName).to.equal('named');
            expect(segment).to.equal('test');
        });
    });

    describe('cache.provision()', () => {

        it('provisions a server cache (before initialization)', async () => {

            const server = Hapi.server();
            await server.cache.provision({ provider: CatboxMemory, name: 'dynamic' });
            const cache = server.cache({ cache: 'dynamic', segment: 'test', expiresIn: 1000 });

            await expect(cache.set('a', 'going in', 0)).to.reject();
            await server.initialize();

            await cache.set('a', 'going in', 0);
            const value = await cache.get('a');
            expect(value).to.equal('going in');
        });

        it('provisions a server cache (after initialization)', async () => {

            const server = Hapi.server();

            await server.initialize();
            await server.cache.provision({ provider: CatboxMemory, name: 'dynamic' });
            const cache = server.cache({ cache: 'dynamic', segment: 'test', expiresIn: 1000 });

            await cache.set('a', 'going in', 0);
            const value = await cache.get('a');
            expect(value).to.equal('going in');
        });

        it('provisions a server cache (promise)', async () => {

            const server = Hapi.server();
            await server.initialize();
            await server.cache.provision({ provider: CatboxMemory, name: 'dynamic' });
            const cache = server.cache({ cache: 'dynamic', segment: 'test', expiresIn: 1000 });

            await cache.set('a', 'going in', 0);
            const value = await cache.get('a');
            expect(value).to.equal('going in');
        });
    });

    describe('control()', () => {

        it('controls the phase of the controlled servers', async () => {

            const server = Hapi.server();
            const controlled1 = Hapi.server();
            const controlled2 = Hapi.server();

            server.control(controlled1);
            server.control(controlled2);

            await server.initialize();
            expect(server._core.phase).to.equal('initialized');
            expect(controlled1._core.phase).to.equal('initialized');
            expect(controlled2._core.phase).to.equal('initialized');

            await server.start();
            expect(server._core.phase).to.equal('started');
            expect(controlled1._core.phase).to.equal('started');
            expect(controlled2._core.phase).to.equal('started');

            await server.stop();
            expect(server._core.phase).to.equal('stopped');
            expect(controlled1._core.phase).to.equal('stopped');
            expect(controlled2._core.phase).to.equal('stopped');
        });
    });

    describe('decorate()', () => {

        it('decorates request with function', async () => {

            const server = Hapi.server();

            const getId = function () {

                return this.info.id;
            };

            server.decorate('request', 'getId', getId);

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => request.getId()
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.match(/^.*\:.*\:.*\:.*\:.*$/);
        });

        it('decorates request with object', async () => {

            const server = Hapi.server();

            const customData = { id: '123' };

            server.decorate('request', 'customData', customData);

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => request.customData
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.equal({ id: '123' });
        });

        it('decorates request (apply)', async () => {

            const server = Hapi.server();

            server.decorate('request', 'uri', (request) => request.server.info.uri, { apply: true });
            server.decorate('request', 'type', (request) => request.server.type, { apply: true });

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => (request.uri + ':' + request.type)
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.equal(server.info.uri + ':tcp');
        });

        it('decorates request (extend)', async () => {

            const server = Hapi.server();

            const getId = function () {

                return this.info.id;
            };

            server.decorate('request', 'getId', getId);

            const getIdExtended = function (existing) {

                return function () {

                    return existing.call(this) + '!';
                };
            };

            server.decorate('request', 'getId', getIdExtended, { extend: true });

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => request.getId()
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.match(/^.*\:.*\:.*\:.*\:.*!$/);
        });

        it('decorates request (extend) with an array', async () => {

            const server = Hapi.server();

            const items = ['one', 'two', 'three'];

            server.decorate('request', 'items', items);
            server.decorate('request', 'items', (existing) => [...existing, 'four'], { extend: true });

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => request.items
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.equal([...items, 'four']);
        });

        it('decorates request (apply + extend)', async () => {

            const server = Hapi.server();

            server.decorate('request', 'uri', (request) => request.server.info.uri, { apply: true });

            const extended = function (existing) {

                return function (request) {

                    const base = existing(request);
                    return base + '!';
                };
            };

            server.decorate('request', 'uri', extended, { apply: true, extend: true });

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => request.uri
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.equal(server.info.uri + '!');
        });

        it('decorates response', async () => {

            const server = Hapi.server();

            const custom = function () {

                return this.header('custom', 'test');
            };

            server.decorate('response', 'custom', custom);

            server.ext('onPreResponse', (request, h) => {

                request.response.custom();
                return h.continue;
            });

            server.route({
                method: 'GET',
                path: '/',
                handler: () => null
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(204);
            expect(res.headers.custom).to.equal('test');
        });

        it('decorates toolkit', async () => {

            const server = Hapi.server();

            const success = function () {

                return this.response({ status: 'ok' });
            };

            server.decorate('toolkit', 'success', success);

            server.route({
                method: 'GET',
                path: '/',
                handler: (request, h) => h.success()
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result.status).to.equal('ok');
        });

        it('decorates toolkit with boolean', async () => {

            const server = Hapi.server();

            const isOk = true;

            server.decorate('toolkit', 'isOk', isOk);

            server.route({
                method: 'GET',
                path: '/',
                handler: (request, h) => h.isOk
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.equal(true);
        });

        it('add new handler', async () => {

            const test = {
                name: 'test',
                register: function (srv, options1) {

                    const handler = function (route, options2) {

                        return (request) => 'success';
                    };

                    srv.decorate('handler', 'bar', handler);
                }
            };

            const server = Hapi.server();
            await server.register(test);

            server.route({
                method: 'GET',
                path: '/',
                handler: {
                    bar: {}
                }
            });

            const res = await server.inject('/');
            expect(res.payload).to.equal('success');
        });

        it('errors on duplicate handler', async () => {

            const server = Hapi.server();
            await server.register(Inert);

            expect(() => {

                server.decorate('handler', 'file', () => { });
            }).to.throw('Handler decoration already defined: file');
        });

        it('errors on unknown handler', () => {

            const server = Hapi.server();

            expect(() => {

                server.route({ method: 'GET', path: '/', handler: { test: {} } });
            }).to.throw('Unknown handler: test');
        });

        it('errors on non-string name', () => {

            const server = Hapi.server();

            expect(() => {

                server.decorate('handler', null);
            }).to.throw('Missing decoration property name');
        });

        it('errors on non-function handler', () => {

            const server = Hapi.server();

            expect(() => {

                server.decorate('handler', 'foo', 'bar');
            }).to.throw('Handler must be a function: foo');
        });

        it('throws on double toolkit decoration', () => {

            const server = Hapi.server();

            server.decorate('toolkit', 'success', () => {

                return this.response({ status: 'ok' });
            });

            expect(() => server.decorate('toolkit', 'success', () => { })).to.throw('Toolkit decoration already defined: success');
        });

        it('throws on internal conflict', () => {

            const server = Hapi.server();

            expect(() => {

                server.decorate('toolkit', 'redirect', () => { });
            }).to.throw('Cannot override the built-in toolkit decoration: redirect');
        });

        it('decorates server', async () => {

            const server = Hapi.server();

            const ok = function (path) {

                server.route({
                    method: 'GET',
                    path,
                    handler: () => 'ok'
                });
            };

            server.decorate('server', 'ok', ok);

            server.ok('/');

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result).to.equal('ok');
        });

        it('decorates server with Map', async () => {

            const server = Hapi.server();

            const itemsMap = new Map();
            itemsMap.set('one', 'One');
            itemsMap.set('two', 'Two');
            itemsMap.set('three', 'Three');

            server.decorate('server', 'itemsMap', itemsMap);

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => request.server.itemsMap
            });

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(200);
            expect(res.result.get('one')).to.equal('One');
            expect(res.result.get('two')).to.equal('Two');
            expect(res.result.get('three')).to.equal('Three');
        });

        it('throws on double server decoration', () => {

            const server = Hapi.server();

            const ok = function (path) {

                server.route({
                    method: 'GET',
                    path,
                    handler: () => 'ok'
                });
            };

            server.decorate('server', 'ok', ok);

            expect(() => {

                server.decorate('server', 'ok', () => { });
            }).to.throw('Server decoration already defined: ok');
        });

        it('throws on server decoration root conflict', () => {

            const server = Hapi.server();

            expect(() => {

                server.decorate('server', 'start', () => { });
            }).to.throw('Cannot override the built-in server interface method: start');
        });

        it('throws on server decoration plugin conflict', () => {

            const server = Hapi.server();

            expect(() => {

                server.decorate('server', 'ext', () => { });
            }).to.throw('Cannot override the built-in server interface method: ext');
        });

        it('throws on invalid decoration name', () => {

            const server = Hapi.server();

            expect(() => {

                server.decorate('server', '_special', () => { });
            }).to.throw('Property name cannot begin with an underscore: _special');
        });
    });

    describe('decorations ()', () => {

        it('shows decorations on request (empty array)', () => {

            const server = Hapi.server();

            expect(server.decorations.request).to.be.empty();
        });

        it('shows decorations on request (single)', () => {

            const server = Hapi.server();

            server.decorate('request', 'a', () => { });

            expect(server.decorations.request).to.equal(['a']);
        });

        it('shows decorations on request (many)', () => {

            const server = Hapi.server();
            const symbol = Symbol('b');

            server.decorate('request', 'a', () => { });
            server.decorate('request', symbol, () => { });

            expect(server.decorations.request).to.equal(['a', symbol]);
        });

        it('shows decorations on toolkit (empty array)', () => {

            const server = Hapi.server();

            expect(server.decorations.toolkit).to.be.empty();
        });

        it('shows decorations on toolkit (single)', () => {

            const server = Hapi.server();

            server.decorate('toolkit', 'a', () => { });

            expect(server.decorations.toolkit).to.equal(['a']);
        });

        it('shows decorations on toolkit (many)', () => {

            const server = Hapi.server();
            const symbol = Symbol('b');

            server.decorate('toolkit', 'a', () => { });
            server.decorate('toolkit', symbol, () => { });

            expect(server.decorations.toolkit).to.equal(['a', symbol]);
        });

        it('shows decorations on server (empty array)', () => {

            const server = Hapi.server();

            expect(server.decorations.server).to.be.empty();
        });

        it('shows decorations on server (single)', () => {

            const server = Hapi.server();

            server.decorate('server', 'a', () => { });

            expect(server.decorations.server).to.equal(['a']);
        });

        it('shows decorations on server (many)', () => {

            const server = Hapi.server();
            const symbol = Symbol('b');

            server.decorate('server', 'a', () => { });
            server.decorate('server', symbol, () => { });

            expect(server.decorations.server).to.equal(['a', symbol]);
        });
    });

    describe('dependency()', () => {

        it('fails to register single plugin with dependencies', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.dependency('none');
                }
            };

            const server = Hapi.server();
            await server.register(test);
            await expect(server.initialize()).to.reject('Plugin test missing dependency none');
        });

        it('fails to register single plugin with dependencies (plugin)', async () => {

            const test = {
                name: 'test',
                dependencies: 'none',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(test);
            await expect(server.initialize()).to.reject('Plugin test missing dependency none');
        });

        it('fails to register multiple plugins with dependencies', async () => {

            const server = Hapi.server({ port: 80, host: 'localhost' });
            await server.register([internals.plugins.deps1, internals.plugins.deps3]);
            await expect(server.initialize()).to.reject('Plugin deps1 missing dependency deps2');
        });

        it('recognizes dependencies from peer plugins', async () => {

            const b = {
                name: 'b',
                register: Hoek.ignore
            };

            const a = {
                name: 'a',
                register: function (srv, options) {

                    return srv.register(b);
                }
            };

            const c = {
                name: 'c',
                register: function (srv, options) {

                    srv.dependency('b');
                }
            };

            const server = Hapi.server();
            await server.register([a, c]);
        });

        it('errors when missing inner dependencies', async () => {

            const b = {
                name: 'b',
                register: function (srv, options) {

                    srv.dependency('c');
                }
            };

            const a = {
                name: 'a',
                register: function (srv, options) {

                    return srv.register(b);
                }
            };

            const server = Hapi.server({ port: 80, host: 'localhost' });
            await server.register(a);
            await expect(server.initialize()).to.reject('Plugin b missing dependency c');
        });

        it('errors when missing inner dependencies (plugin)', async () => {

            const b = {
                name: 'b',
                dependencies: 'c',
                register: Hoek.ignore
            };

            const a = {
                name: 'a',
                register: function (srv, options) {

                    return srv.register(b);
                }
            };

            const server = Hapi.server({ port: 80, host: 'localhost' });
            await server.register(a);
            await expect(server.initialize()).to.reject('Plugin b missing dependency c');
        });
    });

    describe('encoder()', () => {

        it('adds custom encoder with higher priority than built in encoders', async () => {

            const data = '{"test":"true"}';

            const server = Hapi.server({ compression: { minBytes: 1 }, routes: { compression: { test: { some: 'option' } } } });

            const encoder = (options) => {

                expect(options).to.equal({ some: 'option' });
                return Zlib.createGzip();
            };

            server.encoder('test', encoder);
            server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
            await server.start();

            const uri = 'http://localhost:' + server.info.port;
            const zipped = await new Promise((resolve) => Zlib.gzip(Buffer.from(data), (ignoreErr, compressed) => resolve(compressed)));
            const { res, payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip, deflate, test' }, payload: data });
            expect(res.headers['content-encoding']).to.equal('test');
            expect(payload.toString()).to.equal(zipped.toString());
            await server.stop();
        });
    });

    describe('events', () => {

        it('extends server events', async () => {

            const server = Hapi.server();

            const updates = [];
            server.event({ name: 'test', channels: ['x', 'y'] });

            server.events.on({ name: 'test', channels: 'x' }, (update) => updates.push({ id: 'server', channel: 'x', update }));

            let plugin;
            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.events.on({ name: 'test', channels: 'y' }, (update) => updates.push({ id: 'plugin', channel: 'y', update }));
                    plugin = srv;
                }
            };

            server.events.on('test', (update) => updates.push({ id: 'server', update }));

            await server.register(test);

            server.events.emit('test', 1);
            server.events.emit({ name: 'test', channel: 'x' }, 2);
            plugin.events.emit({ name: 'test', channel: 'y' }, 3);

            expect(updates).to.equal([
                { id: 'server', update: 1 },
                { id: 'server', channel: 'x', update: 2 },
                { id: 'server', update: 2 },
                { id: 'server', update: 3 },
                { id: 'plugin', channel: 'y', update: 3 }
            ]);
        });

        it('filters log by tags', async () => {

            const server = Hapi.server();
            const log = server.events.once({ name: 'log', filter: ['x'] });
            server.log('a');
            server.log(['b']);
            server.log(['c', 'x'], 'test 1');
            server.log(['d', 'x'], 'test 2');

            const [event] = await log;
            expect(event.data).to.equal('test 1');
        });
    });

    describe('expose()', () => {

        it('exposes an api', async () => {

            const server = Hapi.server();

            await server.register(internals.plugins.test1);
            expect(internals.routesList(server)).to.equal(['/test1']);
            expect(server.plugins.test1.add(1, 3)).to.equal(4);
            expect(server.plugins.test1.glue('1', '3')).to.equal('13');
        });

        it('exposes an api (scope without scope)', async () => {

            const server = Hapi.server();

            const plugin = {
                name: 'test1',
                version: '1.0.0',
                register: function (srv, options) {

                    srv.expose('x', { y: 1 }, { scope: true });
                }
            };

            await server.register(plugin);
            expect(server.plugins.test1.x.y).to.equal(1);
            expect(server.registrations).to.equal({ test1: { version: '1.0.0', name: 'test1', options: undefined } });
        });

        it('exposes an api (drops scope by default)', async () => {

            const server = Hapi.server();

            const plugin = {
                name: '@hapi/test1',
                version: '1.0.0',
                register: function (srv, options) {

                    srv.expose('x', { y: 1 });
                }
            };

            await server.register(plugin);
            expect(server.plugins.test1.x.y).to.equal(1);
            expect(server.registrations).to.equal({ '@hapi/test1': { version: '1.0.0', name: '@hapi/test1', options: undefined } });
        });

        it('exposes an api (keeps scope)', async () => {

            const server = Hapi.server();

            const plugin = {
                name: '@hapi/test1',
                version: '1.0.0',
                register: function (srv, options) {

                    srv.expose('x', { y: 1 }, { scope: true });
                }
            };

            await server.register(plugin);
            expect(server.plugins['@hapi/test1'].x.y).to.equal(1);
            expect(server.registrations).to.equal({ '@hapi/test1': { version: '1.0.0', name: '@hapi/test1', options: undefined } });
        });

        it('exposes an api (rewrites scope)', async () => {

            const server = Hapi.server();

            const plugin = {
                name: '@hapi/test1',
                version: '1.0.0',
                register: function (srv, options) {

                    srv.expose('x', { y: 1 }, { scope: 'underscore' });
                }
            };

            await server.register(plugin);
            expect(server.plugins.hapi__test1.x.y).to.equal(1);
            expect(server.registrations).to.equal({ '@hapi/test1': { version: '1.0.0', name: '@hapi/test1', options: undefined } });
        });
    });

    describe('ext()', () => {

        it('extends onRequest point', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.route({
                        method: 'GET',
                        path: '/b',
                        handler: () => 'b'
                    });

                    const onRequest = (request, h) => {

                        request.setUrl('/b');
                        return h.continue;
                    };

                    srv.ext('onRequest', onRequest);
                }
            };

            const server = Hapi.server();
            await server.register(test);

            expect(internals.routesList(server)).to.equal(['/b']);
            const res = await server.inject('/a');
            expect(res.result).to.equal('b');
        });

        it('returns promise on empty ext handler', async () => {

            const server = Hapi.server();
            const ext = server.ext('onRequest');
            server.route({ path: '/', method: 'GET', handler: () => 'ok' });
            const res = await server.inject('/');
            expect(res.result).to.equal('ok');
            const request = await ext;
            expect(request.response.source).to.equal('ok');
        });

        it('adds multiple ext functions with complex dependencies', async () => {

            // Generate a plugin with a specific index and ext dependencies.

            const pluginCurrier = function (num, deps) {

                const plugin = {
                    name: 'deps' + num,
                    register: function (server, options) {

                        const onRequest = (request, h) => {

                            request.app.complexDeps = request.app.complexDeps || '|';
                            request.app.complexDeps += num + '|';
                            return h.continue;
                        };

                        server.ext('onRequest', onRequest, deps);
                    }
                };

                return plugin;
            };

            const server = Hapi.server();
            server.route({ method: 'GET', path: '/', handler: (request) => request.app.complexDeps });

            await server.register([
                pluginCurrier(1, { after: 'deps2' }),
                pluginCurrier(2),
                pluginCurrier(3, { before: ['deps1', 'deps2'] })
            ]);

            await server.initialize();

            const res = await server.inject('/');
            expect(res.result).to.equal('|3|2|1|');
        });

        it('binds server ext to context (options)', async () => {

            const server = Hapi.server();

            const bind = {
                state: false
            };

            const preStart = function (srv) {

                this.state = true;
            };

            server.ext('onPreStart', preStart, { bind });

            await server.initialize();
            expect(bind.state).to.be.true();
        });

        it('binds server ext to context (argument)', async () => {

            const server = Hapi.server();

            const bind = {
                state: false
            };

            const preStart = (srv, context) => {

                context.state = true;
            };

            server.ext('onPreStart', preStart, { bind });

            await server.initialize();
            expect(bind.state).to.be.true();
        });

        it('binds server ext to context (realm)', async () => {

            const server = Hapi.server();

            const bind = {
                state: false
            };

            server.bind(bind);
            const preStart = function (srv) {

                this.state = true;
            };

            server.ext('onPreStart', preStart);

            await server.initialize();
            expect(bind.state).to.be.true();
        });

        it('extends server actions', async () => {

            const server = Hapi.server();

            let result = '';
            const preStart = function (srv) {

                result += '1';
            };

            server.ext('onPreStart', preStart);

            const postStart = function (srv) {

                result += '2';
            };

            server.ext('onPostStart', postStart);

            const preStop = function (srv) {

                result += '3';
            };

            server.ext('onPreStop', preStop);

            const postStop = function (srv) {

                result += '4';
            };

            server.ext('onPostStop', postStop);

            await server.start();
            expect(result).to.equal('12');

            await server.stop();
            expect(result).to.equal('1234');
        });

        it('extends server actions (single call)', async () => {

            const server = Hapi.server();

            let result = '';
            server.ext([
                {
                    type: 'onPreStart',
                    method: function (srv) {

                        result += '1';
                    }
                },
                {
                    type: 'onPostStart',
                    method: function (srv) {

                        result += '2';
                    }
                },
                {
                    type: 'onPreStop',
                    method: function (srv) {

                        result += '3';
                    }
                },
                {
                    type: 'onPreStop',
                    method: function (srv) {

                        result += '4';
                    }
                }
            ]);

            await server.start();
            expect(result).to.equal('12');

            await server.stop();
            expect(result).to.equal('1234');
        });

        it('combine route extensions', async () => {

            const server = Hapi.server();

            const preAuth = (request, h) => {

                request.app.x = '1';
                return h.continue;
            };

            server.ext('onPreAuth', preAuth);

            const plugin = {
                name: 'test',
                register: function (srv, options) {

                    srv.route({
                        method: 'GET',
                        path: '/',
                        options: {
                            ext: {
                                onPreAuth: {
                                    method: (request, h) => {

                                        request.app.x += '2';
                                        return h.continue;
                                    }
                                }
                            },
                            handler: (request) => request.app.x
                        }
                    });

                    const preAuthSandbox = (request, h) => {

                        request.app.x += '3';
                        return h.continue;
                    };

                    srv.ext('onPreAuth', preAuthSandbox, { sandbox: 'plugin' });
                }
            };

            await server.register(plugin);

            server.route({
                method: 'GET',
                path: '/a',
                handler: (request) => request.app.x
            });

            const res1 = await server.inject('/');
            expect(res1.result).to.equal('123');

            const res2 = await server.inject('/a');
            expect(res2.result).to.equal('1');
        });

        it('calls method after plugin', async () => {

            const x = {
                name: 'x',
                register: function (srv, options) {

                    srv.expose('a', 'b');
                }
            };

            const server = Hapi.server();

            expect(server.plugins.x).to.not.exist();

            let called = false;
            const preStart = function (srv) {

                expect(srv.plugins.x.a).to.equal('b');
                called = true;
            };

            server.ext('onPreStart', preStart, { after: 'x' });

            await server.register(x);
            await server.initialize();
            expect(called).to.be.true();
        });

        it('calls method before start', async () => {

            const server = Hapi.server();

            let called = false;
            const preStart = function (srv) {

                called = true;
            };

            server.ext('onPreStart', preStart);

            await server.initialize();
            expect(called).to.be.true();
        });

        it('calls method before start even if plugin not registered', async () => {

            const server = Hapi.server();

            let called = false;
            const preStart = function (srv) {

                called = true;
            };

            server.ext('onPreStart', preStart, { after: 'x' });

            await server.initialize();
            expect(called).to.be.true();
        });

        it('fails to start server when after method fails', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    const preStart1 = function (inner) { };

                    srv.ext('onPreStart', preStart1);

                    const preStart2 = function (inner) {

                        throw new Error('Not in the mood');
                    };

                    srv.ext('onPreStart', preStart2);
                }
            };

            const server = Hapi.server();
            await server.register(test);
            await expect(server.initialize()).to.reject('Not in the mood');
        });

        it('errors when added after initialization', async () => {

            const server = Hapi.server();

            await server.initialize();
            expect(() => {

                server.ext('onPreStart', () => { });
            }).to.throw('Cannot add onPreStart (after) extension after the server was initialized');
        });
    });

    describe('log()', () => {

        it('emits a log event', async () => {

            const server = Hapi.server();

            let count = 0;
            server.events.once('log', (event, tags) => {

                ++count;
                expect(event.data).to.equal('log event 1');
            });

            server.events.once('log', (event, tags) => {

                ++count;
                expect(event.data).to.equal('log event 1');
            });

            server.log('1', 'log event 1');

            server.events.once('log', (event, tags) => {

                ++count;
                expect(event.data).to.equal('log event 2');
            });

            server.log(['2'], 'log event 2');
            await Hoek.wait(10);
            expect(count).to.equal(3);
        });

        it('emits a log event (function data)', async () => {

            const server = Hapi.server();
            const log = server.events.once('log');
            server.log('test', () => 123);
            const [event] = await log;
            expect(event.data).to.equal(123);
        });

        it('emits a log event and print to console', async () => {

            const server = Hapi.server({ debug: { log: 'implementation' } });

            server.events.once('log', (event, tags) => {

                expect(event.data).to.equal('log event 1');
            });

            const log = new Promise((resolve) => {

                const orig = console.error;
                console.error = function (...args) {

                    console.error = orig;
                    expect(args[0]).to.equal('Debug:');
                    expect(args[1]).to.equal('internal, implementation, error');

                    resolve();
                };
            });

            server.log(['internal', 'implementation', 'error'], 'log event 1');
            await log;
        });

        it('outputs log data to debug console', async () => {

            const server = Hapi.server({ debug: { log: '*' } });

            const log = new Promise((resolve) => {

                const orig = console.error;
                console.error = function (...args) {

                    console.error = orig;
                    expect(args[0]).to.equal('Debug:');
                    expect(args[1]).to.equal('implementation');
                    expect(args[2]).to.equal('\n    {"data":1}');

                    resolve();
                };
            });

            server.log(['implementation'], { data: 1 });
            await log;
        });

        it('outputs log error data to debug console', async () => {

            const server = Hapi.server({ debug: { log: '*' } });

            const log = new Promise((resolve) => {

                const orig = console.error;
                console.error = function (...args) {

                    console.error = orig;
                    expect(args[0]).to.equal('Debug:');
                    expect(args[1]).to.equal('implementation');
                    expect(args[2]).to.contain('\n    Error: test\n    at');
                    resolve();
                };
            });

            server.log(['implementation'], new Error('test'));
            await log;
        });

        it('outputs log data to debug console without data', async () => {

            const server = Hapi.server({ debug: { log: '*' } });

            const log = new Promise((resolve) => {

                const orig = console.error;
                console.error = function (...args) {

                    console.error = orig;
                    expect(args[0]).to.equal('Debug:');
                    expect(args[1]).to.equal('implementation');
                    expect(args[2]).to.equal('');
                    resolve();
                };
            });

            server.log(['implementation']);
            await log;
        });

        it('does not output events when debug disabled', () => {

            const server = Hapi.server({ debug: false });

            let i = 0;
            const orig = console.error;
            console.error = function () {

                ++i;
            };

            server.log(['implementation']);
            console.error('nothing');
            expect(i).to.equal(1);
            console.error = orig;
        });

        it('does not output events when debug.log disabled', () => {

            const server = Hapi.server({ debug: { log: false } });

            let i = 0;
            const orig = console.error;
            console.error = function () {

                ++i;
            };

            server.log(['implementation']);
            console.error('nothing');
            expect(i).to.equal(1);
            console.error = orig;
        });

        it('does not output non-implementation events by default', () => {

            const server = Hapi.server();

            let i = 0;
            const orig = console.error;
            console.error = function () {

                ++i;
            };

            server.log(['xyz']);
            console.error('nothing');
            expect(i).to.equal(1);
            console.error = orig;
        });

        it('emits server log events once', async () => {

            let pc = 0;
            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.events.on('log', (event, tags) => ++pc);
                }
            };

            const server = Hapi.server();

            let sc = 0;
            server.events.on('log', (event, tags) => ++sc);

            await server.register(test);
            server.log('test');
            expect(sc).to.equal(1);
            expect(pc).to.equal(1);
        });

        it('emits log events after handler error when server is started', async () => {

            const server = Hapi.server({ debug: false });

            const updates = [];
            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.events.on('log', (event, tags) => updates.push(event.tags));
                    srv.events.on('response', (request) => updates.push('response'));
                    srv.events.on({ name: 'request', channels: 'error' }, (request, err) => updates.push({ name: 'request', channels: 'error' }));
                }
            };

            server.route({
                method: 'GET',
                path: '/',
                handler: (request) => {

                    request.server.log('1');
                    throw new Error('2');
                }
            });

            await server.register(test);
            await server.start();

            const res = await server.inject('/');
            expect(res.statusCode).to.equal(500);
            await Hoek.wait(10);
            expect(updates).to.equal([['1'], { name: 'request', channels: 'error' }, 'response']);
            await server.stop();
        });

        it('outputs logs for all server log events with a wildcard', async () => {

            const server = Hapi.server({ debug: { log: '*' } });

            const log = new Promise((resolve) => {

                const orig = console.error;
                console.error = function (...args) {

                    console.error = orig;
                    expect(args[0]).to.equal('Debug:');
                    expect(args[1]).to.equal('foobar');
                    expect(args[2]).to.equal('\n    {"data":1}');
                    resolve();
                };
            });

            server.log(['foobar'], { data: 1 });
            await log;
        });

        it('outputs logs for all request log events with a wildcard', async () => {

            const server = Hapi.server({ debug: { request: '*' } });

            const expectedLogs = [
                ['Debug:', 'handler, error']
            ];

            const log = new Promise((resolve) => {

                const orig = console.error;
                console.error = function (...args) {

                    expect(args).to.contain(expectedLogs.shift());
                    if (expectedLogs.length === 0) {
                        console.error = orig;
                        resolve();
                    }
                };
            });

            server.inject('/', () => { });
            await log;
        });
    });

    describe('lookup()', () => {

        it('returns route based on id', () => {

            const server = Hapi.server();
            server.route({
                method: 'GET',
                path: '/',
                options: {
                    handler: () => null,
                    id: 'root',
                    app: { test: 123 }
                }
            });

            const root = server.lookup('root');
            expect(root.path).to.equal('/');
            expect(root.settings.app.test).to.equal(123);
        });

        it('returns null on unknown route', () => {

            const server = Hapi.server();
            const root = server.lookup('root');
            expect(root).to.be.null();
        });

        it('throws on missing id', () => {

            const server = Hapi.server();
            expect(() => {

                server.lookup();
            }).to.throw('Invalid route id: ');
        });
    });

    describe('match()', () => {

        it('returns route based on path', () => {

            const server = Hapi.server();

            server.route({
                method: 'GET',
                path: '/',
                options: {
                    handler: () => null,
                    id: 'root'
                }
            });

            server.route({
                method: 'GET',
                path: '/abc',
                options: {
                    handler: () => null,
                    id: 'abc'
                }
            });

            server.route({
                method: 'POST',
                path: '/abc',
                options: {
                    handler: () => null,
                    id: 'post'
                }
            });

            server.route({
                method: 'GET',
                path: '/{p}/{x}',
                options: {
                    handler: () => null,
                    id: 'params'
                }
            });

            server.route({
                method: 'GET',
                path: '/abc',
                vhost: 'example.com',
                options: {
                    handler: () => null,
                    id: 'vhost'
                }
            });

            expect(server.match('GET', '/').settings.id).to.equal('root');
            expect(server.match('GET', '/none')).to.equal(null);
            expect(server.match('GET', '/abc').settings.id).to.equal('abc');
            expect(server.match('get', '/').settings.id).to.equal('root');
            expect(server.match('post', '/abc').settings.id).to.equal('post');
            expect(server.match('get', '/a/b').settings.id).to.equal('params');
            expect(server.match('GET', '/abc', 'example.com').settings.id).to.equal('vhost');
        });

        it('throws on missing method', () => {

            const server = Hapi.server();
            expect(() => {

                server.match();
            }).to.throw('Invalid method: ');
        });

        it('throws on invalid method', () => {

            const server = Hapi.server();
            expect(() => {

                server.match(5);
            }).to.throw('Invalid method: 5');
        });

        it('throws on missing path', () => {

            const server = Hapi.server();
            expect(() => {

                server.match('get');
            }).to.throw('Invalid path: ');
        });

        it('throws on invalid path type', () => {

            const server = Hapi.server();
            expect(() => {

                server.match('get', 5);
            }).to.throw('Invalid path: 5');
        });

        it('throws on invalid path prefix', () => {

            const server = Hapi.server();
            expect(() => {

                server.match('get', '5');
            }).to.throw('Invalid path: 5');
        });

        it('throws on invalid path', () => {

            const server = Hapi.server();
            server.route({
                method: 'GET',
                path: '/{p}',
                handler: () => null
            });

            expect(() => {

                server.match('GET', '/%p');
            }).to.throw('Invalid path: /%p');
        });

        it('throws on invalid host type', () => {

            const server = Hapi.server();
            expect(() => {

                server.match('get', '/a', 5);
            }).to.throw('Invalid host: 5');
        });
    });

    describe('method()', () => {

        it('adds server method using arguments', async () => {

            const server = Hapi.server();

            const test = {
                name: 'test',
                register: function (srv, options) {

                    const method = function (methodNext) {

                        return methodNext(null);
                    };

                    srv.method('log', method);
                }
            };

            await server.register(test);
        });

        it('adds server method with plugin bind', async () => {

            const server = Hapi.server();

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.bind({ x: 1 });
                    const method = function () {

                        return this.x;
                    };

                    srv.method('log', method);
                }
            };

            await server.register(test);
            const result = server.methods.log();
            expect(result).to.equal(1);
        });

        it('adds server method with method bind', async () => {

            const server = Hapi.server();

            const test = {
                name: 'test',
                register: function (srv, options) {

                    const method = function () {

                        return this.x;
                    };

                    srv.method('log', method, { bind: { x: 2 } });
                }
            };

            await server.register(test);

            const result = server.methods.log();
            expect(result).to.equal(2);
        });

        it('adds server method with method and ext bind', async () => {

            const server = Hapi.server();

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.bind({ x: 1 });
                    const method = function () {

                        return this.x;
                    };

                    srv.method('log', method, { bind: { x: 2 } });
                }
            };

            await server.register(test);

            const result = server.methods.log();
            expect(result).to.equal(2);
        });
    });

    describe('path()', () => {

        it('sets local path for directory route handler', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.path(Path.join(__dirname, '..'));

                    srv.route({
                        method: 'GET',
                        path: '/handler/{file*}',
                        handler: {
                            directory: {
                                path: './'
                            }
                        }
                    });
                }
            };

            const server = Hapi.server({ routes: { files: { relativeTo: __dirname } } });
            await server.register(Inert);
            await server.register(test);

            const res = await server.inject('/handler/package.json');
            expect(res.statusCode).to.equal(200);
        });

        it('throws when plugin sets undefined path', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.path();
                }
            };

            const server = Hapi.server();
            await expect(server.register(test)).to.reject('relativeTo must be a non-empty string');
        });
    });

    describe('register()', () => {

        it('registers plugin with options', async () => {

            const server = Hapi.server();

            const test = {
                name: 'test',

                register: function (srv, options) {

                    expect(options.something).to.be.true();
                    expect(srv.realm.pluginOptions).to.equal(options);
                }
            };

            await server.register({ plugin: test, options: { something: true } });
        });

        it('registers a required plugin', async () => {

            const server = Hapi.server();

            const test = {
                plugin: {
                    name: 'test',
                    register: function (srv, options) {

                        expect(options.something).to.be.true();
                    }
                }
            };

            await server.register({ plugin: test, options: { something: true } });
        });

        it('rejects on bad plugin (missing name)', async () => {

            const plugin = {
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await expect(server.register(plugin)).to.reject();
        });

        it('rejects on bad plugin (empty pkg)', async () => {

            const plugin = {
                pkg: {},
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await expect(server.register(plugin)).to.reject();
        });

        it('returns plugin error', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    throw new Error('from plugin');
                }
            };

            const server = Hapi.server();
            await expect(server.register(test)).to.reject('from plugin');
        });

        it('sets version to 0.0.0 if missing', async () => {

            const test = {
                pkg: {
                    name: 'steve'
                },
                register: function (srv, options) {

                    srv.route({
                        method: 'GET',
                        path: '/',
                        handler: () => srv.version
                    });
                }
            };

            const server = Hapi.server();
            await server.register(test);
            expect(server.registrations.steve.version).to.equal('0.0.0');

            const res = await server.inject('/');
            expect(res.result).to.equal(require('../package.json').version);
        });

        it('exposes plugin registration information', async () => {

            const test = {
                multiple: true,
                pkg: {
                    name: 'bob',
                    version: '1.2.3'
                },
                register: function (srv, options) {

                    srv.route({ method: 'GET', path: '/', handler: () => srv.version });
                }
            };

            const server = Hapi.server();

            await server.register({ plugin: test, options: { foo: 'bar' } });
            const bob = server.registrations.bob;
            expect(bob).to.exist();
            expect(bob).to.be.an.object();
            expect(bob.version).to.equal('1.2.3');
            expect(bob.options.foo).to.equal('bar');

            const res = await server.inject('/');
            expect(res.result).to.equal(require('../package.json').version);
        });

        it('prevents plugin from multiple registrations', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.route({ method: 'GET', path: '/a', handler: () => 'a' });
                }
            };

            const server = Hapi.server({ host: 'example.com' });
            await server.register(test);
            await expect(server.register(test)).to.reject('Plugin test already registered');
        });

        it('allows plugin multiple registrations (plugin)', async () => {

            const test = {
                name: 'test',
                multiple: true,
                register: function (srv, options) {

                    srv.app.x = srv.app.x ? srv.app.x + 1 : 1;
                }
            };

            const server = Hapi.server();
            await server.register(test);
            await server.register(test);
            expect(server.app.x).to.equal(2);
        });

        it('registers multiple plugins', async () => {

            const server = Hapi.server();
            let log = null;
            server.events.once('log', (event, tags) => {

                log = [event, tags];
            });

            await server.register([internals.plugins.test1, internals.plugins.test2]);
            expect(internals.routesList(server)).to.equal(['/test1', '/test2']);
            expect(log[1].test).to.equal(true);
            expect(log[0].data).to.equal('abc');
        });

        it('registers multiple plugins (verbose)', async () => {

            const server = Hapi.server();
            let log = null;
            server.events.once('log', (event, tags) => {

                log = [event, tags];
            });

            await server.register([{ plugin: internals.plugins.test1 }, { plugin: internals.plugins.test2 }]);
            expect(internals.routesList(server)).to.equal(['/test1', '/test2']);
            expect(log[1].test).to.equal(true);
            expect(log[0].data).to.equal('abc');
        });

        it('registers a child plugin', async () => {

            const server = Hapi.server();
            await server.register(internals.plugins.child);
            const res = await server.inject('/test1');
            expect(res.result).to.equal('testing123');
        });

        it('registers a plugin with routes path prefix', async () => {

            const server = Hapi.server();
            await server.register(internals.plugins.test1, { routes: { prefix: '/xyz' } });

            expect(server.plugins.test1.prefix).to.equal('/xyz');
            const res = await server.inject('/xyz/test1');
            expect(res.result).to.equal('testing123');
        });

        it('registers a plugin with routes path prefix (plugin options)', async () => {

            const server = Hapi.server();
            await server.register({ plugin: internals.plugins.test1, routes: { prefix: '/abc' } }, { routes: { prefix: '/xyz' } });

            expect(server.plugins.test1.prefix).to.equal('/abc');
            const res = await server.inject('/abc/test1');
            expect(res.result).to.equal('testing123');
        });

        it('register a plugin once (plugin options)', async () => {

            let count = 0;
            const b = {
                name: 'b',
                register: function (srv, options) {

                    ++count;
                }
            };

            const a = {
                name: 'a',
                register: async function (srv, options) {

                    await srv.register({ plugin: b, once: true });
                }
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(a);
            await server.initialize();
            expect(count).to.equal(1);
        });

        it('registers plugins and adds options to realm that routes can access', async () => {

            const server = Hapi.server();

            const foo = {
                name: 'foo',
                register: function (srv, options) {

                    expect(options.something).to.be.true();
                    expect(srv.realm.pluginOptions).to.equal(options);

                    srv.route({
                        method: 'GET', path: '/foo', handler: (request, h) => {

                            expect(request.route.realm.pluginOptions).to.equal(options);
                            expect(h.realm.pluginOptions).to.equal(options);
                            return 'foo';
                        }
                    });
                }
            };

            const bar = {
                name: 'bar',
                register: function (srv, options) {

                    expect(options.something).to.be.false();
                    expect(srv.realm.pluginOptions).to.equal(options);

                    srv.route({
                        method: 'GET', path: '/bar', handler: (request, h) => {

                            expect(request.route.realm.pluginOptions).to.equal(options);
                            expect(h.realm.pluginOptions).to.equal(options);
                            return 'bar';
                        }
                    });
                }
            };

            const plugins = [
                { plugin: foo, options: { something: true } },
                { plugin: bar, options: { something: false } }
            ];

            await server.register(plugins);

            const res1 = await server.inject('/foo');
            expect(res1.result).to.equal('foo');

            const res2 = await server.inject('/bar');
            expect(res2.result).to.equal('bar');
        });

        it('registers a plugin with routes path prefix and plugin root route', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.route({
                        method: 'GET',
                        path: '/',
                        handler: () => 'ok'
                    });
                }
            };

            const server = Hapi.server();
            await server.register(test, { routes: { prefix: '/xyz' } });

            const res = await server.inject('/xyz');
            expect(res.result).to.equal('ok');
        });

        it('ignores the type of the plugin value', async () => {

            const a = function () { };
            a.plugin = {
                name: 'a',
                register: function (srv, options) {

                    srv.route({
                        method: 'GET',
                        path: '/',
                        handler: () => 'ok'
                    });
                }
            };

            const server = Hapi.server();
            await server.register(a, { routes: { prefix: '/xyz' } });

            const res = await server.inject('/xyz');
            expect(res.result).to.equal('ok');
        });

        it('ignores unknown plugin properties', async () => {

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.route({
                        method: 'GET',
                        path: '/',
                        handler: () => 'ok'
                    });
                },
                other: {}
            };

            const server = Hapi.server();
            await server.register(a);
        });

        it('ignores unknown plugin properties (with options)', async () => {

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.route({
                        method: 'GET',
                        path: '/',
                        handler: () => 'ok'
                    });
                },
                other: {}
            };

            const server = Hapi.server();
            await server.register({ plugin: a });
        });

        it('registers a child plugin with parent routes path prefix', async () => {

            const server = Hapi.server();
            await server.register(internals.plugins.child, { routes: { prefix: '/xyz' } });

            const res = await server.inject('/xyz/test1');
            expect(res.result).to.equal('testing123');
        });

        it('registers a child plugin with parent routes vhost prefix', async () => {

            const server = Hapi.server();
            await server.register(internals.plugins.child, { routes: { vhost: 'example.com' } });

            const res = await server.inject({ url: '/test1', headers: { host: 'example.com' } });
            expect(res.result).to.equal('testing123');
        });

        it('registers a child plugin with parent routes path prefix and inner register prefix', async () => {

            const server = Hapi.server();
            await server.register({ plugin: internals.plugins.child, options: { routes: { prefix: '/inner' } } }, { routes: { prefix: '/xyz' } });

            const res = await server.inject('/xyz/inner/test1');
            expect(res.result).to.equal('testing123');
        });

        it('registers a child plugin with parent routes vhost prefix and inner register vhost', async () => {

            const server = Hapi.server();
            await server.register({ plugin: internals.plugins.child, options: { routes: { vhost: 'example.net' } } }, { routes: { vhost: 'example.com' } });

            const res = await server.inject({ url: '/test1', headers: { host: 'example.com' } });
            expect(res.result).to.equal('testing123');
        });

        it('registers a plugin with routes vhost', async () => {

            const server = Hapi.server();
            await server.register(internals.plugins.test1, { routes: { vhost: 'example.com' } });

            const res1 = await server.inject('/test1');
            expect(res1.statusCode).to.equal(404);

            const res2 = await server.inject({ url: '/test1', headers: { host: 'example.com' } });
            expect(res2.result).to.equal('testing123');
        });

        it('registers a plugin with routes vhost (plugin options)', async () => {

            const server = Hapi.server();
            await server.register({ plugin: internals.plugins.test1, routes: { vhost: 'example.org' } }, { routes: { vhost: 'example.com' } });

            const res1 = await server.inject('/test1');
            expect(res1.statusCode).to.equal(404);

            const res2 = await server.inject({ url: '/test1', headers: { host: 'example.org' } });
            expect(res2.result).to.equal('testing123');
        });

        it('sets multiple dependencies in one statement', async () => {

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.dependency(['b', 'c']);
                }
            };

            const b = {
                name: 'b',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await server.initialize();
        });

        it('sets multiple dependencies in one statement (versioned)', async () => {

            const a = {
                name: 'a',
                version: '0.1.2',
                register: function (srv, options) {

                    srv.dependency({
                        b: '1.x.x',
                        c: '2.x.x'
                    });
                }
            };

            const b = {
                name: 'b',
                version: '1.2.3',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                version: '2.3.4',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await server.initialize();
        });

        it('sets multiple dependencies in plugin (versioned)', async () => {

            const a = {
                name: 'a',
                version: '0.1.2',
                dependencies: {
                    b: '1.x.x',
                    c: '2.x.x'
                },
                register: Hoek.ignore
            };

            const b = {
                name: 'b',
                version: '1.2.3',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                version: '2.3.4',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await server.initialize();
        });

        it('sets multiple dependencies in plugin', async () => {

            const a = {
                name: 'a',
                dependencies: ['b', 'c'],
                register: Hoek.ignore
            };

            const b = {
                name: 'b',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await server.initialize();
        });

        it('sets multiple dependencies in multiple statements', async () => {

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.dependency('b');
                    srv.dependency('c');
                }
            };

            const b = {
                name: 'b',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await server.initialize();
        });

        it('sets multiple dependencies in multiple locations', async () => {

            const a = {
                name: 'a',
                dependencies: 'c',
                register: function (srv, options) {

                    srv.dependency('b');
                }
            };

            const b = {
                name: 'b',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await server.initialize();
        });

        it('register a plugin once per server', async () => {

            let count = 0;
            const b = {
                name: 'b',
                register: function (srv, options) {

                    ++count;
                }
            };

            const a = {
                name: 'a',
                register: function (srv, options) {

                    return srv.register(b, { once: true });
                }
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(a);
            await server.initialize();
            expect(count).to.equal(1);
        });

        it('register a plugin once (plugin)', async () => {

            let count = 0;
            const b = {
                name: 'b',
                once: true,
                register: function (srv, options) {

                    ++count;
                }
            };

            const a = {
                name: 'a',
                register: function (srv, options) {

                    return srv.register(b);
                }
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(a);
            await server.initialize();
            expect(count).to.equal(1);
        });

        it('throws when once used with plugin options', async () => {

            const a = {
                name: 'a',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await expect(server.register({ plugin: a, options: {}, once: true })).to.reject();
        });

        it('throws when once is false', async () => {

            const b = {
                name: 'b',
                register: Hoek.ignore
            };

            const a = {
                name: 'a',
                register: function (srv, options) {

                    return srv.register(b);
                }
            };

            const server = Hapi.server();
            await server.register(b);
            await expect(server.register(a)).to.reject(Error, 'Plugin b already registered');
        });

        it('throws when dependencies is an object', async () => {

            const a = {
                name: 'a',
                dependencies: { b: true },
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await expect(server.register(a)).to.reject();
        });

        it('throws when dependencies contain something else than a string', async () => {

            const a = {
                name: 'a',
                dependencies: [true],
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await expect(server.register(a)).to.reject();
        });

        it('exposes server decorations to next register', async () => {

            const server = Hapi.server();

            const b = {
                name: 'b',
                register: function (srv, options) {

                    if (typeof srv.a !== 'function') {
                        throw new Error('Missing decoration');
                    }
                }
            };

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.decorate('server', 'a', () => {

                        return 'a';
                    });
                }
            };

            await server.register([a, b]);
            await server.initialize();
        });

        it('exposes server decorations to dependency (dependency first)', async () => {

            const server = Hapi.server();

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.decorate('server', 'a', () => {

                        return 'a';
                    });
                }
            };

            const b = {
                name: 'b',
                register: function (srv, options) {

                    const after = function (srv2) {

                        if (typeof srv2.a !== 'function') {
                            throw new Error('Missing decoration');
                        }
                    };

                    srv.dependency('a', after);
                }
            };

            await server.register([a, b]);
            await server.initialize();
        });

        it('exposes server decorations to dependency (dependency second)', async () => {

            const server = Hapi.server();

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.decorate('server', 'a', () => 'a');
                }
            };

            const b = {
                name: 'b',
                register: function (srv, options) {

                    srv.realm.x = 1;
                    const after = function (srv2) {

                        expect(srv2.realm.x).to.equal(1);
                        if (typeof srv2.a !== 'function') {
                            throw new Error('Missing decoration');
                        }
                    };

                    srv.dependency('a', after);
                }
            };

            await server.register([b, a]);
            await server.initialize();
        });

        it('exposes server decorations to next register when nested', async () => {

            const server = Hapi.server();

            const a = {
                name: 'a',
                register: function (srv, options) {

                    srv.decorate('server', 'a', () => {

                        return 'a';
                    });
                }
            };

            const b = {
                name: 'b',
                register: async function (srv, options) {

                    await srv.register(a);
                    if (typeof srv.a !== 'function') {
                        throw new Error('Missing decoration');
                    }
                }
            };

            await server.register([b]);
            await server.initialize();
        });

        it('validates node version', async () => {

            const test = {
                name: 'test',
                requirements: {
                    node: '>=8.x.x'
                },
                register: function (srv, options) { }
            };

            const server = Hapi.server();
            await expect(server.register(test)).to.not.reject();
        });

        it('validates node version, allowing prereleases', async (flags) => {

            const test = {
                name: 'test',
                requirements: {
                    node: '>=8.x.x'
                },
                register: function (srv, options) { }
            };

            const origVersion = process.version;
            Object.defineProperty(process, 'version', { value: 'v100.0.0-beta' });
            flags.onCleanup = () => Object.defineProperty(process, 'version', { value: origVersion });

            const server = Hapi.server();
            await expect(server.register(test)).to.not.reject();
        });

        it('errors on invalid node version', async () => {

            const test = {
                name: 'test',
                requirements: {
                    node: '4.x.x'
                },
                register: function (srv, options) { }
            };

            const server = Hapi.server();
            await expect(server.register(test)).to.reject(`Plugin test requires node version 4.x.x but found ${process.version}`);
        });

        it('validates hapi version', async () => {

            const test = {
                name: 'test',
                requirements: {
                    hapi: '>=17.x.x'
                },
                register: function (srv, options) { }
            };

            const server = Hapi.server();
            await expect(server.register(test)).to.not.reject();
        });

        it('validates hapi version, allowing prereleases', async () => {

            const test = {
                name: 'test',
                requirements: {
                    hapi: '>=17.x.x'
                },
                register: function (srv, options) { }
            };

            const server = Hapi.server();
            server.version = '100.0.0-beta';
            await expect(server.register(test)).to.not.reject();
        });

        it('errors on invalid hapi version', async () => {

            const test = {
                name: 'test',
                requirements: {
                    hapi: '4.x.x'
                },
                register: function (srv, options) { }
            };

            const server = Hapi.server();
            await expect(server.register(test)).to.reject(`Plugin test requires hapi version 4.x.x but found ${Pkg.version}`);
        });

        it('validates plugin version, allowing prereleases', async () => {

            const a = {
                name: 'a',
                version: '0.1.2',
                dependencies: {
                    b: '>=3.x.x',
                    c: '>=2.x.x'
                },
                register: Hoek.ignore
            };

            const b = {
                name: 'b',
                version: '4.0.0-beta',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                version: '2.3.4',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await expect(server.initialize()).to.not.reject();
        });

        it('errors on invalid plugin version', async () => {

            const a = {
                name: 'a',
                version: '0.1.2',
                dependencies: {
                    b: '3.x.x',
                    c: '2.x.x'
                },
                register: Hoek.ignore
            };

            const b = {
                name: 'b',
                version: '1.2.3',
                register: Hoek.ignore
            };

            const c = {
                name: 'c',
                version: '2.3.4',
                register: Hoek.ignore
            };

            const server = Hapi.server();
            await server.register(b);
            await server.register(c);
            await server.register(a);
            await expect(server.initialize()).to.reject('Plugin a requires b version 3.x.x but found 1.2.3');
        });
    });

    describe('render()', () => {

        it('renders view', async () => {

            const server = Hapi.server();
            await server.register(Vision);
            server.views({
                engines: { html: Handlebars },
                path: __dirname + '/templates'
            });

            const rendered = await server.render('test', { title: 'test', message: 'hapi' });
            expect(rendered).to.exist();
            expect(rendered).to.contain('hapi');
        });
    });

    describe('views()', () => {

        it('requires plugin with views', async () => {

            const test = {
                name: 'test',
                register: function (srv, options) {

                    srv.path(__dirname);

                    const views = {
                        engines: { 'html': Handlebars },
                        path: './templates/plugin'
                    };

                    srv.views(views);
                    if (Object.keys(views).length !== 2) {
                        throw new Error('plugin.view() modified options');
                    }

                    srv.route([
                        {
                            path: '/view',
                            method: 'GET',
                            handler: (request, h) => h.view('test', { message: options.message })
                        },
                        {
                            path: '/file',
                            method: 'GET',
                            handler: { file: './templates/plugin/test.html' }
                        }
                    ]);

                    const onRequest = (request, h) => {

                        if (request.path === '/ext') {
                            return h.view('test', { message: 'grabbed' }).takeover();
                        }

                        return h.continue;
                    };

                    srv.ext('onRequest', onRequest);
                }
            };

            const server = Hapi.server();
            await server.register([Inert, Vision]);
            await server.register({ plugin: test, options: { message: 'viewing it' } });

            const res1 = await server.inject('/view');
            expect(res1.result).to.equal('<h1>viewing it</h1>');

            const res2 = await server.inject('/file');
            expect(res2.result).to.equal('<h1>{{message}}</h1>');

            const res3 = await server.inject('/ext');
            expect(res3.result).to.equal('<h1>grabbed</h1>');
        });
    });
});


internals.routesList = function (server) {

    return server.table().filter((route) => route.method === 'get').map((route) => route.path);
};


internals.plugins = {
    auth: {
        name: 'auth',
        register: function (server, options) {

            const scheme = function (srv, authOptions) {

                const settings = Hoek.clone(authOptions);

                return {
                    authenticate: (request, h) => {

                        const req = request.raw.req;
                        const authorization = req.headers.authorization;
                        if (!authorization) {
                            throw Boom.unauthorized(null, 'Basic');
                        }

                        const parts = authorization.split(/\s+/);

                        if (parts[0] &&
                            parts[0].toLowerCase() !== 'basic') {

                            throw Boom.unauthorized(null, 'Basic');
                        }

                        if (parts.length !== 2) {
                            throw Boom.badRequest('Bad HTTP authentication header format', 'Basic');
                        }

                        const credentialsParts = Buffer.from(parts[1], 'base64').toString().split(':');
                        if (credentialsParts.length !== 2) {
                            throw Boom.badRequest('Bad header internal syntax', 'Basic');
                        }

                        const username = credentialsParts[0];
                        const password = credentialsParts[1];

                        const { isValid, credentials } = settings.validateFunc(username, password);
                        if (!isValid) {
                            return h.unauthenticated(Boom.unauthorized('Bad username or password', 'Basic'), { credentials });
                        }

                        return h.authenticated({ credentials });
                    }
                };
            };

            server.auth.scheme('basic', scheme);

            const loadUser = function (username, password) {

                if (username === 'john') {
                    return { isValid: password === '12345', credentials: { user: 'john' } };
                }

                return { isValid: false };
            };

            server.auth.strategy('basic', 'basic', { validateFunc: loadUser });
            server.auth.default('basic');

            server.auth.scheme('special', () => {

                return { authenticate: function () { } };
            });

            server.auth.strategy('special', 'special', {});
        }
    },
    child: {
        name: 'child',
        register: function (server, options) {

            if (options.routes) {
                return server.register(internals.plugins.test1, options);
            }

            return server.register(internals.plugins.test1);
        }
    },
    deps1: {
        name: 'deps1',
        register: function (server, options) {

            const after = function (srv) {

                srv.expose('breaking', srv.plugins.deps2.breaking);
            };

            server.dependency('deps2', after);

            const onRequest = (request, h) => {

                request.app.deps = request.app.deps || '|';
                request.app.deps += '1|';
                return h.continue;
            };

            server.ext('onRequest', onRequest, { after: 'deps3' });
        }
    },
    deps2: {
        name: 'deps2',
        register: function (server, options) {

            const onRequest = (request, h) => {

                request.app.deps = request.app.deps || '|';
                request.app.deps += '2|';
                return h.continue;
            };

            server.ext('onRequest', onRequest, { after: 'deps3', before: 'deps1' });
            server.expose('breaking', 'bad');
        }
    },
    deps3: {
        name: 'deps3',
        register: function (server, options) {

            const onRequest = (request, h) => {

                request.app.deps = request.app.deps || '|';
                request.app.deps += '3|';
                return h.continue;
            };

            server.ext('onRequest', onRequest);
        }
    },
    test1: {
        name: 'test1',
        version: '1.0.0',
        register: function (server, options) {

            const handler = (request) => {

                return 'testing123' + ((server.settings.app && server.settings.app.my) || '');
            };

            server.route({ path: '/test1', method: 'GET', handler });

            server.expose({
                add: function (a, b) {

                    return a + b;
                }
            });

            const glue = function (a, b) {

                return a + b;
            };

            server.expose('glue', glue);
            server.expose('prefix', server.realm.modifiers.route.prefix);
        }
    },
    test2: {
        pkg: {
            name: 'test2',
            version: '1.0.0'
        },
        name: 'test2',
        register: function (server, options) {

            server.route({
                path: '/test2',
                method: 'GET',
                handler: () => 'testing123'
            });

            server.log('test', 'abc');
        }
    }
};
